package de.e2it3t.schulverwaltung.ui;

import de.e2it3t.schulverwaltung.App;
import de.e2it3t.schulverwaltung.data.TimeHelper;
import de.e2it3t.schulverwaltung.data.Lehreinheit;
import de.e2it3t.schulverwaltung.data.LehreinheitTable;
import java.awt.Color;
import java.awt.Cursor;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.sql.Time;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;

/**
 *Panel, welches den Stundenplan zeichnet. <br>
 * <br>
 * Eine pixelgenaue Darstellung ist mathematisch aufwändig und kniffelig. Wir
 * wollen die Tage und Zeiten pixelgenau und maßstabsgetreu zeichen. Dafür
 * müssen wir etwas Aufwnd betreiben:<br>
 * <br>
 * Erst einmal sehen wir einen Abstand auf der oberen und linken Seite für
 * die Legende vor. Dann berechnen wir pixelgenau die Position eines Tages
 * und einer Stunde und deren zugehörige Weite. Mit Hilfe dieser Werte
 * können wir das Stundenplangitter zeichnen und die Position jeder
 * einzelnen Lehreinheit auf den Pixel genau errechnen.<br>
 * <br>
 * Wir könnten uns diesen Aufwand sparen, einfach mit double-Genauigkeit
 * rechenen und anschließend die erechneten Werte in Pixel konvertieren.
 * Dies führt aber dazu, dass Lehreinheiten und das Stundenplangitter sich
 * an manchen Stellen berührern und an manchen nicht, da die Rundungen um
 * Pixel abweichen. Das erweckt einen Splampigen Eindruck und das knabbert
 * am Progrmmiererstolz :-P
 * 
 * @author Viktor Reiser
 */
class LehreinheitNicePanel extends JPanel implements MouseMotionListener,
        KeyListener, MouseListener {
    
    private AppView owner;
    private int klasseId;
    
    private List<LehreinheitTable> lehreinheitenData
            = new ArrayList<LehreinheitTable>();
    private List<LehreinheitTable> originalLehreinheitenData
            = new ArrayList<LehreinheitTable>();
    
    
    // Farbe für den Legendentext
    private Color legendColor = new Color(100, 100, 100);
    // Farbe für die Linien des Stundenplangitters
    private Color gridColor = new Color(200, 200, 200);
    // Farbe der Lehreinheit
    private Color blockColor = new Color(255, 251, 184);
    // Randfarbe der Lehreinheit
    private Color blockBorderColor = blockColor.darker();
    // Farbe für den Text in einer Lehreinheit
    private Color textColor = new Color(0, 0, 0);

    // Uhrzeitenbereich, welcher im Stundenplan gezeichnet wird (in Stunden)
    private int timeMin;
    private int timeMax;

    // Position ...
    private int[] timeY;
    // ... und Weite einer gezeichneten Zeitenzeile (in Pixel)
    private int[] timeHeight;

    // angezeigte Tage, was auch als Legende dient
    String[] days = new String[] {"Mo", "Di", "Mi", "Do", "Fr"};

    // Position ...
    private int[] dayX;
    // ... und Weite einer gezeichneten Tagesspalte (in Pixel)
    private int[] dayWidth;

    private Map<Integer, Rectangle> lehreinheitRect
            = new HashMap<Integer, Rectangle>();

    // Höhe eines Strings
    private int textHeight;

    // Abstand oben und links für die Legende (in Pixel)
    private int top;
    private int left;
    private int right;
    private int bottom;
    private int pad = 2;

    private int lastWidth;
    private int lastHeight;
    
    
    private Lehreinheit originalLehreinheit;
    private ActionType actionType = ActionType.NONE;
    private int actionCursor;
    private int actionX;
    private int actionY;
    private int previousDay = -1;


    @SuppressWarnings("LeakingThisInConstructor")
    public LehreinheitNicePanel() {
        addMouseMotionListener(this);
        addKeyListener(this);
        addMouseListener(this);
    }

    // PUBLIC ==================================================================

    public void loadView(AppView owner, int klasseId) {
        this.owner = owner;
        this.klasseId = klasseId;
        
        Calendar cal = Calendar.getInstance();
        
        cal.setTimeInMillis(App.getConfig().getMinimumTime().getTime());
        timeMin = cal.get(Calendar.HOUR_OF_DAY);
        
        cal.setTimeInMillis(App.getConfig().getMaximumTime().getTime());
        timeMax = cal.get(Calendar.HOUR_OF_DAY) + (cal.get(Calendar.MINUTE) == 0 ? 0 : 1);
        
        if (klasseId == 0) {
            lehreinheitenData = new ArrayList<LehreinheitTable>();
        } else {
            lehreinheitenData = App.getDatabase().getLehreinheitenTable(klasseId);
        }

        lehreinheitRect.clear();

        repaint();
    }
    
    
    // ZEICHNEN ================================================================

    @Override
    public void paint(Graphics g) {
        super.paint(g);

        Graphics2D g2 = (Graphics2D)g;

        setupParameters(g2);
        setupDays();
        setupTime();

        drawBaseGrid(g2);

        if (lehreinheitenData != null) {
            for (LehreinheitTable lehreinheit : lehreinheitenData) {
                drawLehreinheit(g2, lehreinheit);
            }
        }

        lastWidth = getWidth();
        lastHeight = getHeight();
    }

    
    private void setupParameters(Graphics2D g) {
        textHeight = g.getFontMetrics().getHeight();

        top = textHeight + 4; // oben lassen wir Platz für die Tagestexte
        left = 30; // links lassen wir Platz für die Stundenzahlen
        right = getWidth();
        bottom = getHeight();
    }

    private void setupDays() {
        if (dayX == null || dayX.length != days.length) {
            dayX = new int[days.length];
            dayWidth = new int[days.length];
        } else if (lastWidth == getWidth() && lastHeight == getHeight()) {
            return;
        }

        int availableDaySpace = right - left;
        int nextX = left;

        for (int i = 0; i < days.length; i++) {
            int withoutSeparator = availableDaySpace - (days.length - i);
            int span = withoutSeparator / (days.length - i);

            dayX[i] = nextX;
            dayWidth[i] = span - 1;

            nextX += span + 1;
            availableDaySpace -= span + 1;
        }
    }

    private void setupTime() {
        int timeSpan = timeMax - timeMin;

        if (timeY == null || timeY.length != timeSpan) {
            timeY = new int[timeSpan];
            timeHeight = new int[timeSpan];
        } else if (lastWidth == getWidth() && lastHeight == getHeight()) {
            return;
        }

        int availableTimeSpace = bottom - top;
        int nextY = top;

        for (int i = 0; i < timeSpan; i++) {
            int withoutSeparator = availableTimeSpace - (timeSpan - i);
            int span = withoutSeparator / (timeSpan - i);

            timeY[i] = nextY;
            timeHeight[i] = span - 1;

            nextY += span + 1;
            availableTimeSpace -= span + 1;
        }
    }

    private void drawBaseGrid(Graphics2D g) {
        FontMetrics fm = g.getFontMetrics();

        for (int i = 0; i < dayX.length; i++) {
            int legendX = dayX[i] + (dayWidth[i] - fm.stringWidth(days[i])) / 2;

            g.setColor(gridColor);
            g.drawLine(dayX[i] - 1, top, dayX[i] - 1, bottom);

            g.setColor(legendColor);
            g.drawString(days[i], legendX, top - 4);
        }

        for (int i = 0; i < timeY.length; i++) {
            g.setColor(gridColor);
            g.drawLine(left, timeY[i] - 1, right, timeY[i] - 1);

            if (i != 0) {
                int legendY = timeY[i] + textHeight / 2 - 2;

                g.setColor(legendColor);
                g.drawString((i + timeMin) + "", 2, legendY);
            }
        }

        g.setColor(gridColor);
        g.drawLine(right - 1, top,        right - 1, bottom - 1);
        g.drawLine(left - 1,  bottom - 1, right - 1, bottom - 1);
    }

    private void drawLehreinheit(Graphics2D g, LehreinheitTable lehreinheit) {
        Rectangle rect = getLehreinheitRect(lehreinheit);

        if (rect.isEmpty()) {
            return;
        }
        
        boolean isAction = actionType != ActionType.NONE
                && originalLehreinheitenData != null
                && originalLehreinheit != null
                && lehreinheit == getActionLehreinheit(originalLehreinheit);

        g.setColor(isAction ? blockColor.darker() : blockColor);
        g.fill(rect);
        g.setColor(isAction ? blockBorderColor.darker() : blockBorderColor);
        g.draw(rect);

        String fach = lehreinheit.getFach();

        int fx = (int)(rect.x + rect.width / 2.0
                - g.getFontMetrics().stringWidth(fach) / 2.0 + 0.5);
        int fy = (int)(rect.y + rect.height / 2.0
                + textHeight / 2.0  - 3 + 0.5);

        g.setColor(textColor);
        g.drawString(fach, fx, fy);
    }

    private Rectangle getLehreinheitRect(Lehreinheit lehreinheit) {
        Rectangle rect = lehreinheitRect.get(lehreinheit.getId());

        if (rect != null && lastWidth == getWidth() && lastHeight == getHeight()) {
            return rect;
        }

        rect = new Rectangle(0, 0, 0, 0);
        Calendar cal = Calendar.getInstance();
        int day = lehreinheit.getTag();

        lehreinheitRect.put(lehreinheit.getId(), rect);

        if (day < 0 && day >= dayX.length) {
            return rect;
        }

        cal.setTimeInMillis(lehreinheit.getUhrzeitBeginn().getTime());
        int topHours = cal.get(Calendar.HOUR_OF_DAY);
        int topMinutes =  cal.get(Calendar.MINUTE);

        cal.setTimeInMillis(lehreinheit.getUhrzeitEnde().getTime());
        int bottomHours = cal.get(Calendar.HOUR_OF_DAY);
        int bottomMinutes = cal.get(Calendar.MINUTE);

        if (bottomHours < timeMin || topHours >= timeMax) {
            return rect;
        }

        topHours -= timeMin;
        bottomHours -= timeMin;

        rect.x = dayX[day] + pad;
        rect.y = timeY[topHours] + timeHeight[topHours] * topMinutes / 60 + pad;
        rect.width = dayWidth[day] - pad * 2;
        rect.height = -pad * 2;

        int span = bottomHours - topHours;

        if (span == 0) {
            rect.height += timeHeight[bottomHours] * bottomMinutes / 60
                    - timeHeight[topHours] * topMinutes / 60;
        } else {
            for (int i = 0; i <= span; i++) {
                if (i == 0) {
                    rect.height += timeHeight[topHours]
                            - timeHeight[topHours] * topMinutes / 60 + 1;
                }

                if (i == span) {
                    if (bottomMinutes == 0) {
                        rect.height -= 1;
                    } else {
                        rect.height += timeHeight[bottomHours]
                                * bottomMinutes / 60;
                    }
                } else {
                    if (i != 0) {
                        rect.height += timeHeight[topHours + i] + 2;
                    }
                }
            }
        }

        return rect;
    }

    
    // STUNDENPLAN HELFER ======================================================
    
    /**
     * Erhalte eine Lehreinheit anhand der gegebenen Werte.
     * 
     * @param day Tag der Lehreinheit
     * @param time Zeitpunkt innerhalb der Lehreinheit
     * 
     * @return Lehreinheit oder {@code null}, wenn keine Lehreinheit zu diesen
     * Werten passt
     */
    private LehreinheitTable getLehreinheit(int day, Time time) {
        List<LehreinheitTable> lehreinheiten = originalLehreinheitenData;
        
        if (lehreinheiten == null) {
            lehreinheiten = lehreinheitenData;
        }
        
        for (LehreinheitTable lehreinheit : lehreinheiten) {
            if (lehreinheit.getTag() == day
                    && TimeHelper.compareTime(
                        time, lehreinheit.getUhrzeitBeginn()) >= 0
                    && TimeHelper.compareTime(
                        time, lehreinheit.getUhrzeitEnde()) < 0) {
                return lehreinheit;
            }
        }

        return null;
    }
    
    /**
     * Erhalte den Zeitpunkt des Klicks auf den Stundenplan.
     * 
     * @param y y-Koordinate des Klicks
     * 
     * @return Zeitpunkt im Stundenplan
     */
    private Time getTimeByClick(int y) {
        int hours = -1;
        int minutes = -1;

        for (int i = 0; i < timeMax - timeMin - 1; i++) {
            if (y >= timeY[i] && y < timeY[i + 1]) {
                hours = i + timeMin;
                minutes = 60 * (y - timeY[i]) / timeHeight[i];
            }
        }

        if (hours == -1) {
            if (y <= timeY[0]) {
                hours = timeMin;
                minutes = 0;
            } else {
                hours = timeMax;
                minutes = 0;
            }
        }

        Calendar cal = Calendar.getInstance();

        cal.set(2000, 1, 1, 0, 0, 0);
        cal.set(Calendar.HOUR_OF_DAY, hours);
        cal.set(Calendar.MINUTE, minutes);

        return TimeHelper.clampToMinutesInterval(
                new Time(cal.getTimeInMillis()),
                App.getConfig().getMinutesInterval());
    }
    
    /**
     * Erhalte den Tag des Klicks auf den Stundenplan.
     * 
     * @param x x-Koordinate des Klicks
     * 
     * @return Wert des Tages
     */
    private int getDayByClick(int x) {
        if (x < dayX[0]) {
            return 0;
        }
        
        for (int i = 0; i < days.length - 1; i++) {
            if (dayX[i] <= x && dayX[i] + dayWidth[i] + 1 >= x) {
                return i;
            }
        }
        
        return days.length - 1;
    }
    
    
    // KLICK OPERATIONEN =======================================================
    
    private void performMenuClick(MouseEvent e) {
        int day = getDayByClick(e.getX());
        Time time = getTimeByClick(e.getY());

        if (day == -1 || time == null) {
            return;
        }
        
        Lehreinheit lehreinheit = getLehreinheit(day, time);
        
        if (lehreinheit != null) {
            new LehreinheitMenu(lehreinheit).show(this, e.getX(), e.getY());
        } else {
            new LehreinheitMenu(day, time).show(this, e.getX(), e.getY());
        }
    }
    
    private void performDoubleClick(MouseEvent e) {
        int day = getDayByClick(e.getX());
        Time time = getTimeByClick(e.getY());

        if (day == -1 || time == null) {
            return;
        }

        Lehreinheit lehreinheit = getLehreinheit(day, time);

        if (lehreinheit != null) {
            new LehreinheitDialog(owner, klasseId, lehreinheit.getId())
                    .setVisible(true);
        }
    }
    
    
    // MAUS DRAG OPERATIONEN ===================================================
    
    private void trackActionOnMouseMove(MouseEvent e) {
        int x = e.getX();
        int y = e.getY();
        
        actionCursor = Cursor.DEFAULT_CURSOR;
        actionType = ActionType.NONE;
        originalLehreinheit = null;
        actionX = x;
        actionY = y;
        previousDay = -1;
        
        originalLehreinheitenData = null;
        
        for (Lehreinheit lehreinheit : lehreinheitenData) {
            Rectangle rect = getLehreinheitRect(lehreinheit);
            
            if (!rect.isEmpty() && x > rect.x && x < rect.x + rect.width) {
               if (Math.abs(y - rect.y) <= pad) {
                   actionCursor = Cursor.N_RESIZE_CURSOR;
                   actionType = ActionType.RESIZE_TOP;
               } else if (Math.abs(y - rect.y - rect.height) <= pad) {
                   actionCursor = Cursor.S_RESIZE_CURSOR;
                   actionType = ActionType.RESIZE_BOTTOM;
               } else if (y >= rect.y && y < rect.y + rect.height) {
                   if ((e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0) {
                       actionCursor = Cursor.MOVE_CURSOR;
                       actionType = ActionType.DRAG_INSERT;
                   } else {
                       actionType = ActionType.DRAG_MOVE;
                   }
               }
            }
            
            if (actionType != ActionType.NONE) {
                originalLehreinheit = lehreinheit;
                break;
            }
        }
        
        if (getCursor().getType() != actionCursor) {
            setCursor(new Cursor(actionCursor));
        }
    }
    
    private void trackActionOnMouseDrag(MouseEvent e) {
        if (actionType == ActionType.NONE
                || (e.getModifiersEx() & MouseEvent.BUTTON1_DOWN_MASK) == 0) {
            return;
        }
        
        
        if (originalLehreinheitenData == null) {
            if (actionType == ActionType.DRAG_MOVE
                    && (e.getModifiersEx() & InputEvent.CTRL_DOWN_MASK) != 0) {
                actionType = ActionType.DRAG_INSERT;
                actionCursor = Cursor.MOVE_CURSOR;
            }
            
            originalLehreinheitenData = lehreinheitenData;
            lehreinheitenData = new ArrayList<LehreinheitTable>();
            
            for (LehreinheitTable lehreinheit : originalLehreinheitenData) {
                try {
                    lehreinheitenData.add(lehreinheit.clone());
                } catch (CloneNotSupportedException ex) {
                    throw new RuntimeException(ex);
                }
            }
        }
        
        if (getCursor().getType() != actionCursor) {
            setCursor(new Cursor(actionCursor));
        }
        
        
        for (LehreinheitTable l : lehreinheitenData) {
            if (l.getTag() == originalLehreinheit.getTag()) {
                lehreinheitRect.remove(l.getId());
            }
        }
        
        switch (actionType) {
            case RESIZE_TOP:
                performResizeTop(e);
                break;
            case RESIZE_BOTTOM:
                performResizeBottom(e);
                break;
            case DRAG_INSERT:
                performDragInsert(e);
                break;
            case DRAG_MOVE:
                performDragMove(e);
                break;
            default:
                return;
        }
        
        repaint();
    }
    
    private void performResizeTop(MouseEvent e) {
        resetToOriginal();
        
        Time time = TimeHelper.minTime(
                getTimeByClick(e.getY()),
                TimeHelper.addMinutes(
                    originalLehreinheit.getUhrzeitEnde(),
                    -App.getConfig().getMinimumMinutes()));
        int compare = TimeHelper.compareTime(
                time, originalLehreinheit.getUhrzeitBeginn());
        
        if (compare < 0) {
            time = moveUpAction(originalLehreinheit.getUhrzeitBeginn(),
                time, originalLehreinheit.getTag(), true);
        } else if (compare == 0) {
            return;
        }
        
        getActionLehreinheit(originalLehreinheit).setUhrzeitBeginn(time);
        lehreinheitRect.remove(originalLehreinheit.getId());
    }
    
    private void performResizeBottom(MouseEvent e) {
        resetToOriginal();
        
        Time time = TimeHelper.maxTime(
                getTimeByClick(e.getY()),
                TimeHelper.addMinutes(
                    originalLehreinheit.getUhrzeitBeginn(),
                    App.getConfig().getMinimumMinutes()));
        int compare = TimeHelper.compareTime(
                originalLehreinheit.getUhrzeitEnde(), time);
        
        if (compare < 0) {
            time = moveDownAction(originalLehreinheit.getUhrzeitEnde(),
                time, originalLehreinheit.getTag(), true);
        } else if (compare == 0) {
            return;
        }
        
        getActionLehreinheit(originalLehreinheit).setUhrzeitEnde(time);
        lehreinheitRect.remove(originalLehreinheit.getId());
    }
    
    private void performDragInsert(MouseEvent e) {
        resetToOriginal();
        
        Time insert = getTimeByClick(e.getY());
        int day = getDayByClick(e.getX());
        int minuteDiff = TimeHelper.getMinutesDiff
                (insert, getTimeByClick(actionY));
        int lehreinheitMinutes = TimeHelper.getMinutesDiff(
                originalLehreinheit.getUhrzeitBeginn(),
                originalLehreinheit.getUhrzeitEnde());
        
        if (previousDay != day) {
            for (LehreinheitTable l : lehreinheitenData) {
                if (l.getTag() == previousDay) {
                    lehreinheitRect.remove(l.getId());
                }
            }
        }
        
        previousDay = day;
        
        Lehreinheit lehreinheit = getLehreinheit(day, insert);
        
        if (lehreinheit != null && lehreinheit != originalLehreinheit) {
            insert = TimeHelper.compareTime(
                    insert,
                    TimeHelper.addMinutes(
                        lehreinheit.getUhrzeitBeginn(),
                        lehreinheitMinutes / 2)) < 0
                    ? lehreinheit.getUhrzeitBeginn()
                    : lehreinheit.getUhrzeitEnde();
        }
        
        Time begin = moveUpAction(insert,
                TimeHelper.addMinutes(insert, -lehreinheitMinutes),
                day, false);
        Time end = moveDownAction(insert, 
                TimeHelper.addMinutes(insert, lehreinheitMinutes),
                day, false);
        
        int topMinutes = TimeHelper.getMinutesDiff(begin, insert);
        int bottomMinutes = TimeHelper.getMinutesDiff(insert, end);
        int availableMinutes = topMinutes + bottomMinutes;
        
        if (availableMinutes < lehreinheitMinutes) {
            return;
        }
        
        int startMinutes = TimeHelper.clampToMinutesInterval(
                topMinutes - (lehreinheitMinutes / 2 + minuteDiff),
                App.getConfig().getMinutesInterval());
        
        if (startMinutes < 0) {
            startMinutes = 0;
        }
        
        if (startMinutes + lehreinheitMinutes > availableMinutes) {
            startMinutes = availableMinutes - lehreinheitMinutes;
        }
        
        begin = TimeHelper.addMinutes(begin, startMinutes);
        end = TimeHelper.addMinutes(begin, lehreinheitMinutes);
        
        moveUpAction(insert, begin, day, true);
        moveDownAction(insert, end, day, true);
        
        Lehreinheit actionLehreinheit = getActionLehreinheit(originalLehreinheit);
        actionLehreinheit.setUhrzeitBeginn(begin);
        actionLehreinheit.setUhrzeitEnde(end);
        actionLehreinheit.setTag(day);
        lehreinheitRect.remove(originalLehreinheit.getId());
    }
    
    private void performDragMove(MouseEvent e) {
        resetToOriginal();
        
        int minuteDiff = TimeHelper.getMinutesDiff(
                getTimeByClick(e.getY()), getTimeByClick(actionY));
        int lehreinheitMinutes = TimeHelper.getMinutesDiff(
                originalLehreinheit.getUhrzeitBeginn(),
                originalLehreinheit.getUhrzeitEnde());
        Time newEnde;
        Time newBeginn;
        
        if (minuteDiff < 0) {
            newEnde = originalLehreinheit.getUhrzeitEnde();
            newEnde = moveDownAction(
                    newEnde,
                    TimeHelper.addMinutes(newEnde, -minuteDiff),
                    originalLehreinheit.getTag(), true);
            newBeginn = TimeHelper.addMinutes(newEnde, -lehreinheitMinutes);
        } else if (minuteDiff > 0) {
            newBeginn = originalLehreinheit.getUhrzeitBeginn();
            newBeginn = moveUpAction(
                    newBeginn,
                    TimeHelper.addMinutes(newBeginn, -minuteDiff),
                    originalLehreinheit.getTag(), true);
            newEnde = TimeHelper.addMinutes(newBeginn, lehreinheitMinutes);
        } else {
            return;
        }
        
        Lehreinheit actionLehreinheit = getActionLehreinheit(originalLehreinheit);
        actionLehreinheit.setUhrzeitBeginn(newBeginn);
        actionLehreinheit.setUhrzeitEnde(newEnde);
        lehreinheitRect.remove(originalLehreinheit.getId());
    }
    
    private Time moveUpAction(Time from, Time to, int day, boolean move) {
        to = TimeHelper.maxTime(App.getConfig().getMinimumTime(), to);
        Lehreinheit lehreinheit = null;
        int lastCompare = Integer.MIN_VALUE;
        
        for (Lehreinheit lh : originalLehreinheitenData) {
            if (lh != originalLehreinheit && lh.getTag() == day) {
                int compare = TimeHelper.compareTime(
                    lh.getUhrzeitEnde(), from);
                
                if (compare <= 0 && compare > lastCompare) {
                    lastCompare = compare;
                    lehreinheit = lh;
                }
            }
        }
        
        if (lehreinheit == null) {
            return to;
        }
        
        int minuteDiff = TimeHelper.getMinutesDiff(
                lehreinheit.getUhrzeitEnde(), to);
            
        if (minuteDiff >= 0) {
            return to;
        }
        
        Time newBeginn = moveUpAction(
                lehreinheit.getUhrzeitBeginn(),
                TimeHelper.addMinutes(lehreinheit.getUhrzeitBeginn(), minuteDiff),
                day, move);
        Time newEnd = TimeHelper.addMinutes(
                newBeginn, TimeHelper.getMinutesDiff(
                    lehreinheit.getUhrzeitBeginn(),
                    lehreinheit.getUhrzeitEnde()));
        
        if (move) {
            Lehreinheit lh = getActionLehreinheit(lehreinheit);
            lh.setUhrzeitBeginn(newBeginn);
            lh.setUhrzeitEnde(newEnd);
            lehreinheitRect.remove(lh.getId());
        }
        
        return newEnd;
    }
    
    private Time moveDownAction(Time from, Time to, int day, boolean move) {
        to = TimeHelper.minTime(App.getConfig().getMaximumTime(), to);
        Lehreinheit lehreinheit = null;
        int lastCompare = Integer.MAX_VALUE;
        
        for (Lehreinheit lh : originalLehreinheitenData) {
            if (lh != originalLehreinheit && lh.getTag() == day) {
                int compare = TimeHelper.compareTime(
                    lh.getUhrzeitBeginn(), from);
                
                if (compare >= 0 && compare < lastCompare) {
                    lastCompare = compare;
                    lehreinheit = lh;
                }
            }
        }
        
        if (lehreinheit == null) {
            return to;
        }
        
        int minuteDiff = TimeHelper.getMinutesDiff(
                to, lehreinheit.getUhrzeitBeginn());
            
        if (minuteDiff >= 0) {
            return to;
        }
        
        Time newEnd = moveDownAction(
                lehreinheit.getUhrzeitEnde(),
                TimeHelper.addMinutes(lehreinheit.getUhrzeitEnde(), -minuteDiff),
                day, move);
        Time newBeginn = TimeHelper.addMinutes(
                newEnd, -TimeHelper.getMinutesDiff(
                    lehreinheit.getUhrzeitBeginn(),
                    lehreinheit.getUhrzeitEnde()));
        
        if (move) {
            Lehreinheit lh = getActionLehreinheit(lehreinheit);
            lh.setUhrzeitBeginn(newBeginn);
            lh.setUhrzeitEnde(newEnd);
            lehreinheitRect.remove(lh.getId());
        }
        
        return newBeginn;
    }
    
    private Lehreinheit getOriginalLehreinheit(Lehreinheit lehreinheit) {
        for (Lehreinheit original : originalLehreinheitenData) {
            if (lehreinheit.getId() == original.getId()) {
                return original;
            }
        }
        
        return null;
    }
    
    private Lehreinheit getActionLehreinheit(Lehreinheit lehreinheit) {
        for (Lehreinheit action : lehreinheitenData) {
            if (lehreinheit.getId() == action.getId()) {
                return action;
            }
        }
        
        return null;
    }
    
    private void resetToOriginal() {
        for (int i = 0; i < lehreinheitenData.size(); i++) {
            Lehreinheit original = originalLehreinheitenData.get(i);
            Lehreinheit now = lehreinheitenData.get(i);
            
            now.setTag(original.getTag());
            now.setUhrzeitBeginn(original.getUhrzeitBeginn());
            now.setUhrzeitEnde(original.getUhrzeitEnde());
        }
    }
    
    private void persistAfterAction() {
        for (int i = 0; i < lehreinheitenData.size(); i++) {
            Lehreinheit original = originalLehreinheitenData.get(i);
            Lehreinheit modified = lehreinheitenData.get(i);
            
            if (original.getTag() != modified.getTag()
                    || TimeHelper.compareTime(
                    original.getUhrzeitBeginn(), modified.getUhrzeitBeginn()) != 0
                    || TimeHelper.compareTime(
                    original.getUhrzeitEnde(), modified.getUhrzeitEnde()) != 0) {
                App.getDatabase().saveLehreinheit(modified);
            }
        }
        
        actionType = ActionType.NONE;
        originalLehreinheit = null;
        originalLehreinheitenData = null;
        
        repaint();
    }
    
    
    // LISTENER INTERFACE ======================================================
    
    @Override
    public void mouseMoved(MouseEvent e) {
        trackActionOnMouseMove(e);
    }
    
    @Override
    public void mouseClicked(MouseEvent e) {
        if (e.getButton() == MouseEvent.BUTTON3) {
            performMenuClick(e);
        } else if (e.getClickCount() == 2) {
            performDoubleClick(e);
        }
    }
    
    @Override
    public void mouseDragged(MouseEvent e) {
        trackActionOnMouseDrag(e);
    }
    
    @Override
    public void mouseReleased(MouseEvent e) {
        if (actionType != ActionType.NONE && originalLehreinheitenData != null) {
            persistAfterAction();
        }
    }
    
    @Override
    public void mousePressed(MouseEvent e) {
    }

    @Override
    public void mouseEntered(MouseEvent e) {
    }

    @Override
    public void mouseExited(MouseEvent e) {
    }
    
    @Override
    public void keyTyped(KeyEvent e) {
    }

    @Override
    public void keyPressed(KeyEvent e) {
    }

    @Override
    public void keyReleased(KeyEvent e) {
    }

    
    // HELFER KLASSEN ==========================================================
    
    /**
     * Aktionstyp beim für Mausbewegungen.
     */
    private static enum ActionType {
        NONE,
        DRAG_INSERT,
        DRAG_MOVE,
        RESIZE_TOP,
        RESIZE_BOTTOM
    }
    
    /**
     * Rechtsklick Menü.
     */
    private class LehreinheitMenu extends JPopupMenu {
        
        private Lehreinheit lehreinheit;
        private int day;
        private Time time;
        
        
        public LehreinheitMenu(Lehreinheit lehreinheit) {
            this.lehreinheit = lehreinheit;
            
            addEdit();
            addDelete();
        }
        
        public LehreinheitMenu(int day, Time time) {
            this.day = day;
            this.time = time;
            
            addNew();
        }
        
        private void addNew() {
            JMenuItem menu = new JMenuItem("Neu");
            menu.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    new LehreinheitDialog(owner, klasseId, day, time)
                            .setVisible(true);
                }
            });
            add(menu);
        }
        
        private void addEdit() {
            JMenuItem menu = new JMenuItem("Bearbeiten");
            menu.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    new LehreinheitDialog(owner, klasseId, lehreinheit.getId())
                            .setVisible(true);
                }
            });
            add(menu);
        }
        
        private void addDelete() {
            JMenuItem menu = new JMenuItem("Löschen");
            menu.addActionListener(new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    App.getDatabase().deleteLehreinheit(lehreinheit.getId());
                    owner.refreshView();
                }
            });
            add(menu);
        }
    }
}
